﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;

#pragma warning disable 420
namespace Sencha.Serialization.Contracts
{
	public sealed class DataContract
	{
		private static readonly IList<TypeDescription> EmptyTypes = new ReadOnlyCollection<TypeDescription>(new TypeDescription[0]);

		private readonly DataContract m_prototype;
		private readonly ReaderWriterLockCache<TypeDescription, ITypeSerializer> m_serializersByType;

		private readonly Dictionary<Type, TypeDescription> m_types;
		private readonly Dictionary<TypeDescription, IList<TypeDescription>> m_typeHeirs;
		private readonly Dictionary<TypeDescription, TypeDescription> m_typeSerializers;
		private readonly object m_syncObj;
		private volatile int m_version; 


		/// <summary>
		/// Stores, "root" type, used by JsonSerializer to store serialized type(aka "root" type)
		/// </summary>
		internal TypeDescription Root { get; private set; }
		/// <summary>
		/// Incremented at every change, used by Serializers for cache invalidation
		/// </summary>
		internal int Version { get { if (m_prototype == null) return m_version; else return m_prototype.m_version + m_version; } }
		
		public ICollection<TypeDescription> Types { get { return m_types.Values; } }
		public IEnumerable<TypeDescription> AllTypes { get { return m_types.Values.Concat(m_prototype != null ? m_prototype.AllTypes : Enumerable.Empty<TypeDescription>()); } }

		public MetadataInterpreter Interpreter { get; private set; }
		public MetadataDescriber Describer { get; private set; }

		public DataContract()
			: this(null, null, null, MetadataInterpreter.Default, MetadataDescriber.Default)
		{

		}
		public DataContract(DataContract prototype)
			: this(prototype, null, null, MetadataInterpreter.Default, MetadataDescriber.Default)
		{

		}
		public DataContract(DataContract prototype, Type[] knownTypes)
			: this(prototype, null, knownTypes, MetadataInterpreter.Default, MetadataDescriber.Default)
		{

		}
		public DataContract(DataContract prototype, Type[] knownTypes, MetadataInterpreter interpreter, MetadataDescriber describer)
			: this(prototype, null, knownTypes, interpreter, describer)
		{

		}
		public DataContract(Type type)
			: this(null, type, null, MetadataInterpreter.Default, MetadataDescriber.Default)
		{

		}
		public DataContract(Type type, params Type[] knownTypes)
			: this(null, type, knownTypes, MetadataInterpreter.Default, MetadataDescriber.Default)
		{

		}
		public DataContract(Type type, MetadataInterpreter interpreter, MetadataDescriber describer)
			: this(null, type, null, interpreter, describer)
		{

		}
		public DataContract(Type type, Type[] knownTypes, MetadataInterpreter interpreter, MetadataDescriber describer)
			: this(null, type, knownTypes, interpreter, describer)
		{

		}
		public DataContract(DataContract prototype, Type type, Type[] knownTypes, MetadataInterpreter interpreter, MetadataDescriber describer)
		{
			if (interpreter == null)
				interpreter = MetadataInterpreter.Default;
			if (describer == null)
				describer = MetadataDescriber.Default;

			m_prototype = prototype ?? DataContractCache.DefaultContract;
			m_types = new Dictionary<Type, TypeDescription>(80);
			m_typeSerializers = new Dictionary<TypeDescription, TypeDescription>(20);
			m_typeHeirs = new Dictionary<TypeDescription, IList<TypeDescription>>(20);
			m_syncObj = new object();
			m_serializersByType = new ReaderWriterLockCache<TypeDescription, ITypeSerializer>(80);

			this.Interpreter = interpreter;
			this.Describer = describer;

			if (type != null)
				this.Root = this.AddKnownType(type);

			if (knownTypes != null)
			{
				for (int i = 0; i < knownTypes.Length; i++)
					this.AddKnownType(knownTypes[i]);
			}
		}

		public TypeDescription AddKnownType(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var result = (TypeDescription)null;

			if (m_prototype != null && m_prototype.TryGetKnownType(type, out result))
				return result;

			lock (m_syncObj)
			{
				if (m_types.TryGetValue(type, out result))
					return result;

				result = this.Interpreter.Interpret(this, Describer, type);
				Interlocked.Increment(ref m_version);
			}

			return result;
		}
		public TypeDescription GetKnownType(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			TypeDescription typeInfo;

			lock (m_syncObj)
			{
				if (this.TryGetKnownType(type, out typeInfo))
					return typeInfo;
			}

			if (m_prototype != null && m_prototype.TryGetKnownType(type, out typeInfo))
				return typeInfo;

			throw new Exceptions.TypeDescriptionIsNotFound(type);
		}
		public bool TryGetKnownType(Type type, out TypeDescription result)
		{
			if (type == null)
				throw new ArgumentNullException("type");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			lock (m_syncObj)
			{
				if (m_types.TryGetValue(type, out result))
					return true;
			}

			if (m_prototype != null)
				return m_prototype.TryGetKnownType(type, out result);
			else
				return false;
		}
		public TypeDescription[] AddKnownTypes(params Type[] types)
		{
			if (types == null)
				throw new ArgumentNullException("types");
			System.Diagnostics.Contracts.Contract.ForAll(types, t => t != null);
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			return this.AddKnownTypes((IList<Type>)types);
		}
		public TypeDescription[] AddKnownTypes(IList<Type> types)
		{
			if (types == null)
				throw new ArgumentNullException("types");
			System.Diagnostics.Contracts.Contract.ForAll(types, t => t != null);
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var typeInfos = new TypeDescription[types.Count];
			for (int i = 0; i < types.Count; i++)
				typeInfos[i] = this.AddKnownType(types[i]);

			return typeInfos;
		}
		public IEnumerable<TypeDescription> GetTypeHeirs(TypeDescription typeInfo)
		{
			if (typeInfo == null)
				throw new ArgumentNullException("typeInfo");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			IEnumerable<TypeDescription> allHeirs = null;
			IList<TypeDescription> heirs = null;

			if (m_prototype != null)
				allHeirs = m_prototype.GetTypeHeirs(typeInfo);

			lock (m_syncObj)
			{
				if (m_typeHeirs.TryGetValue(typeInfo, out heirs))
				{
					if (allHeirs == null)
						allHeirs = heirs;
					else
						allHeirs = Enumerable.Concat(allHeirs, heirs);
				}
			}


			return allHeirs ?? Enumerable.Empty<TypeDescription>();
		}
		/// <summary>
		/// Get "serializer" type which is used for serialization of current type
		/// </summary>
		public TypeSerializer<T> GetSerializerOfType<T>()
		{
			var type = this.GetKnownType(typeof(T));
			return (TypeSerializer<T>)this.GetSerializerOfType(type);
		}

		internal void AddTypeInfo(TypeDescription info)
		{
			if (info == null)
				throw new ArgumentNullException("info");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			lock (m_syncObj)
			{
				var type = info.GetClrType();
				if (m_types.ContainsKey(type))
					return;
				m_types.Add(type, info);
				Interlocked.Increment(ref m_version);
			}
		}
		internal void AddTypeHeir(TypeDescription baseType, TypeDescription heir)
		{
			if (baseType == null)
				throw new ArgumentNullException("baseType");
			if (heir == null)
				throw new ArgumentNullException("heir");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			lock (m_syncObj)
			{
				var heirs = (IList<TypeDescription>)null;
				if (!m_typeHeirs.TryGetValue(baseType, out heirs))
					m_typeHeirs.Add(baseType, heirs = new List<TypeDescription>());
				heirs.Add(heir);
				Interlocked.Increment(ref m_version);
			}
		}
		internal TypeDescription AddTypeSerializer(TypeDescription serializedType, TypeDescription serializer)
		{
			if (serializedType == null)
				throw new ArgumentNullException("serializedType");
			if (serializer == null)
				throw new ArgumentNullException("serializer");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			lock (m_syncObj)
			{
				var exSerializer = default(TypeDescription);
				if (m_typeSerializers.TryGetValue(serializedType, out exSerializer))
					return exSerializer;

				m_typeSerializers.Add(serializedType, serializer);
				Interlocked.Increment(ref m_version);
			}

			return serializer;
		}
		internal ITypeSerializer GetSerializerOfType(TypeDescription typeInfo)
		{
			if (typeInfo == null)
				throw new ArgumentNullException("typeInfo");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var serializerType = default(TypeDescription);

			var baseType = typeInfo;
			lock (m_syncObj)
			{
				while (baseType != null)
				{
					serializerType = this.GetSerializerTypeFor(baseType);
					if (serializerType != null && ((serializerType.SerializerOptions & TypeSerializerOptions.Inherited) != 0 || baseType == typeInfo))
						break;

					serializerType = null;
					baseType = baseType.BaseType;
				}
			}

			if (serializerType == null)
			{
				baseType = typeInfo;
				serializerType = this.GetDefaultTypeSerializer(typeInfo);
			}


			var serializer = m_serializersByType.FetchValueOrDefault(serializerType);
			if (serializer != null)
				return serializer;

			serializer = m_serializersByType.FetchOrCreateValue(serializerType, () =>
			{
				var type = serializerType.GetClrType();

				// lift generic type
				if ((serializerType.SerializerOptions & TypeSerializerOptions.LiftGenericArgument) != 0 && type.IsGenericType && type.GetGenericArguments().Length == 1)
				{
					type = type.GetGenericTypeDefinition().MakeGenericType(typeInfo.GetClrType());
					baseType = typeInfo;
				}

				var ctr = type.GetConstructor(new[] { typeof(DataContract), typeof(TypeDescription) });
				if (ctr != null)
					return (ITypeSerializer)ctr.Invoke(new object[] { this, baseType });

				ctr = type.GetConstructor(new[] { typeof(TypeDescription) });
				if (ctr != null)
					return (ITypeSerializer)ctr.Invoke(new object[] { baseType });

				ctr = type.GetConstructor(Type.EmptyTypes);
				if (ctr != null)
					return (ITypeSerializer)ctr.Invoke(null);
				else
					throw new Exceptions.NoConstructorDefined(typeof(DataContract), type);
			});

			return serializer;
		}
		private TypeDescription GetSerializerTypeFor(TypeDescription typeInfo)
		{
			var surrogate = default(TypeDescription);
			if (m_typeSerializers.TryGetValue(typeInfo, out surrogate))
				return surrogate;

			if (m_prototype != null)
				return m_prototype.GetSerializerTypeFor(typeInfo);

			return null;

		}
		private TypeDescription GetDefaultTypeSerializer(TypeDescription typeInfo)
		{
			var type = typeInfo.GetClrType();
			if (typeInfo.IsNullable && typeInfo.IsValueType)
				return this.AddKnownType(typeof(Serializers.NullableSerializer<>).MakeGenericType(typeInfo.UnderlyingType.GetClrType()));
			if (typeInfo.IsConvertible)
				return this.AddKnownType(typeof(Serializers.PrimitiveSerializer<>).MakeGenericType(type));
			else if (typeInfo.IsDictionary)
				return this.AddKnownType(typeof(Serializers.DictionarySerializer<>).MakeGenericType(type));
			else if (typeInfo.IsEnumerable)
				return this.AddKnownType(typeof(Serializers.ArraySerializer<>).MakeGenericType(type));
			else
				return this.AddKnownType(typeof(Serializers.ObjectSerializer<>).MakeGenericType(type));
		}

		public override string ToString()
		{
			return string.Format("Count: {0}", this.Types.Count);
		}
	}
}